package jdesktop.appbar.win32;

import jdesktop.appbar.*;
import jdesktop.appbar.AppBar;

import java.awt.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Computes the drag rectangle for the appbar. During a drag operation, clients
 * call {@link #updateDrag} to fetch the desired drag rectangle.
 * <p>
 * This instance manages dragging to different edges by dividing the screen
 * into four triangular areas, with the base of the triangle parallel and adjacent to the edge of the screen.
 * If {@linkplain #floatArea float area} is not null and has non-zero width and height, it defines the space where no edge is selected. If the appBar is
 * {@linkplain WAppBar#isFloatable()} floatable} then it is undocked and floated
 * on the desktop area.
 */
public class DragGuide
{
    protected Logger log = Logger.getLogger(getClass().getName());

    /**
     * The appbar to drag about.
     */
    private AppBarModel model;

    private AppBar owner;

    private Sides allowedEdges;

    // this should be desktop bounds, not screen bounds? If screen bounds, and we
    // have lots of appbars already lined up, then the user may have to drag
    // over other appbars.  todo.
    private Rectangle   screenBounds;

    /**
     * The area that the appbar is dragged into to cause a change from docked to floating.
     * Null if the appbar always remains docked.
     */
    private Rectangle   dragToFloatBounds;


    /**
     * {@code true} when the appbar is being dragged. Dragging is started with a call to
     * {@link #startDrag(java.awt.Point)} and ended by calling {@link #endDrag(boolean)}.
     */
    private boolean     dragging;

    // remember the original values that we change during dragging.
    private Edge        originalEdge;
    private int         originalSize;
    private boolean     originalDocked;

    private Edge        currentEdge;

    // if dragging starts in floating mode, remember the offset from the mouse cursor, so this can be restored, if necessary
    // remember bounding rectangle

    /**
     * The boundary between docked and floating.
     * When inside this rectangle, the appbar is floating. When outise the
     * rectangle, the appbar is docked to one of the sides.
     */
    private Rectangle   floatArea;

    /**
     * If the drag started on a floating window, this stores the drag offset - the offset of the
     * mouse from the top-left of the window.
     */
    private Point       dragWindowOffset = new Point();


    /**
     * @param owner The appBar this class is working for. This provides the state, such as the current edge, dockable
     *  state etc..
     * @param screenBounds This is the draggable bounds for the appbar. (Actually, the appbar can be dragged outside
     * this region, as only the ratio is used in practice.)
     */
    public void init(AppBar owner, Rectangle screenBounds)
    {
        this.screenBounds = screenBounds;
        this.model = owner.getAppBarModel();
        this.owner = owner;
    }



    /**
     * Called to start the window drag from the specified point.
     * @param pt    The current point the mouse is at (screen co-ordinates.)
     */
    public void startDrag(Point pt)
    {
        dragging = true;

        originalDocked = model.isDocked();
        originalEdge = model.getEdge();
        originalSize = model.getBarSize();
        allowedEdges = model.getAllowedEdges();
        floatArea = null;
        if (model.isFloatable())
        {
            floatArea = dragToFloatBounds;
            Point offset = owner.getWindow().getLocation(dragWindowOffset);
            GeomUtil.scale(offset, -1.0);
            offset.move(pt.x, pt.y);
            dragWindowOffset = offset;
        }

        currentEdge = originalEdge;

        if (log.isLoggable(Level.FINER))
            log.finer(String.format("startDrag [%d,%d]", pt.x, pt.y));
    }

    /**
     *
     * @param mouse         The current mouse co-ordinates.
     * @param current       The current rectangle.
     * @return {@code true} if the {@code current} Rectangle was changed.
     */
    public boolean updateDrag(Point mouse, Rectangle current)
    {
        assert dragging;

        boolean changed = false;

        Edge edge = determineEdge(mouse, currentEdge);
        if (log.isLoggable(Level.FINEST))
            log.finest(String.format("Dragging %s %s", mouse.toString(), edge==null ? null : edge.name()));

        if (edge==null)     // floating
        {
            model.setDocked(false);
            model.setFloatingLocation(mouse.x-(current.width/2), mouse.y);
            changed = true;
        }
        /*else if (edge==owner.getEdge())
        {
            // the rectangle always becomes the current desired window bounds.
            // todo - property to control if appBar area is reserved or not.
            owner.setDocked(true);
            owner.getWindow().getBounds(current);
            changed = true;
        }
        */
        else
        {
            changed = true;
            if (allowedEdges.isSet(edge) && model.getEdge()!=edge)
                model.setEdge(edge);
            if (!model.isDocked())
                model.setDocked(true);
        }
        if (changed)
            owner.getBounds(current);
        return changed;
    }

    /**
     * Determines which edge the appbar should be docked to or if it is floating.
     * @param mouse The current mouse screen co-ordinates.
     * @return  The edge the appbar should be docked to, or {@code null}
     *  if it should be floating.
     */
    protected Edge determineEdge(Point mouse, Edge currentEdge)
    {
        if (floatArea!=null && floatArea.contains(mouse))
            return null;

        // the line x==y*cx/cy goes from top-left to bottom-right
        // the line x==cy-y*cx/cy goes from bottom-left to top-right
        int cx = screenBounds.width;
        int cy = screenBounds.height;

        // discard center area? - lines become too close so index of difficulty increases

        // top-right (true) bottom/left (false).
        boolean b1 = (mouse.y*cx/cy)<mouse.x;
        // bottom-right (true) top/left (false),
        boolean b2 = ((cy-mouse.y)*cx/cy)<mouse.x;

        int val = (b1?1:0) + (b2?2:0);
        Edge edge;
        switch (val)
        {
            case 0: edge = Edge.LEFT;break;
            case 1: edge = Edge.TOP;break;
            case 2: edge = Edge.BOTTOM;break;
            case 3: edge = Edge.RIGHT;break;
            default:
               throw new InternalError("expected 0<val<4");
        }

        return edge;
    }

    /**
     * Cancels the drag operation.
     * @param cancel
     */
    public void endDrag(boolean cancel)
    {
        assert dragging;
        dragging = false;

        if (log.isLoggable(Level.FINER))
            log.finer(String.format("endDrag cancel=%b", cancel));
        if (cancel)
        {
            // todo - can't decide between freezing notifications, and freezing the UI.
            // In the code, seems more sensible to freeze notifications.
            boolean update = model.setFreezeNotification(true);
            owner.setUpdateWindow(false);
            model.setEdge(originalEdge);
            model.setBarSize(originalSize);
            model.setDocked(originalDocked);
            model.setFreezeNotification(update);
            owner.setUpdateWindow(true);
            owner.setPositionFromModel();
        }
    }

    public boolean isDragging()
    {
        return dragging;
    }

    public Rectangle getDragToFloatBounds()
    {
        return dragToFloatBounds;
    }

    public void setDragToFloatBounds(Rectangle dragToFloatBounds)
    {
        this.dragToFloatBounds = dragToFloatBounds;
    }
}
